package cc.yaomm.common.basic.excel.base;

import cc.yaomm.common.basic.enums.OSSDirectory;
import cc.yaomm.common.basic.file.FileService;
import cc.yaomm.common.basic.util.SpringContextUtils;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.context.WriteContext;
import com.alibaba.excel.enums.WriteTypeEnum;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelCommonException;
import com.alibaba.excel.exception.ExcelDataConvertException;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.read.metadata.holder.ReadRowHolder;
import com.alibaba.excel.read.metadata.holder.ReadWorkbookHolder;
import com.alibaba.excel.read.metadata.property.ExcelReadHeadProperty;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.google.common.base.Joiner;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Created by XQL on 2021/10/13.
 * 1、支持多行表头（单不知头名称有重复）
 * 2、支持自定义扩展解析数据合并的问题，通过mergeData方法自定义扩展
 */
@Slf4j
public abstract class SuperAnalysisEventListener<T extends SuperImportVO> extends AnalysisEventListener<T> {

    //参数上下文
    protected SuperParamContext paramContext;

    //最大列索引值
    protected int maxColumnIdx = 0;
    //表内重复判断key值集合
    private Set<String> repeatSet = Sets.newHashSet();
    //结果集，此字段不能代码所有数据，待优化：数据量大后可能会需要内容溢出问题
    private List<SuperImportVO> dataList = new ArrayList<>();

    public SuperAnalysisEventListener(SuperParamContext paramContext) {
        this.paramContext = paramContext;
    }

    /**
     * <p>@Description : 异常捕获 </p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2021/10/13 11:21 </p>
     */
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        if (exception instanceof ExcelDataConvertException) {
            ExcelDataConvertException ex = (ExcelDataConvertException) exception;
            Head head = ex.getExcelContentProperty().getHead();
            Annotation[] annotations = ex.getExcelContentProperty().getField().getAnnotations();
            String message = this.getAnnotationMessage(annotations);
            if (!StringUtils.hasText(message)) {
                message = "：数据格式错误，请参考说明填写";
            }
            message = Joiner.on("-").skipNulls().join(head.getHeadNameList()).concat("：").concat(message);
            SuperImportVO importVO = new SuperImportVO();
            importVO.setRowIdx(ex.getRowIndex());
            importVO.setColumnIdxes(Sets.newHashSet(ex.getColumnIndex()));
            importVO.setSuccess(false);
            importVO.setMessage(message);
            dataList.add(importVO);
            log.error("数据转换异常,表头：{}", head.getHeadNameList(), exception);
        } else {
            log.error("导入异常,", exception);
            throw exception;
        }
    }

    /**
     * <p>@Description : 获取注解上的提示语 </p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2022/1/20 10:06 </p>
     */
    private String getAnnotationMessage(Annotation[] annotations) {
        if (annotations.length < 2) { //自身有@ExcelProperty注解
            return null;
        }
        Set<String> msgSet = Arrays.stream(annotations).filter(annotation -> {
            Class<? extends Annotation> aClass = annotation.annotationType();
            return aClass.getName().startsWith("javax.validation.constraints");
        })
                .map(annotation -> {
                    try {
                        InvocationHandler invocationHandler = Proxy.getInvocationHandler(annotation);
                        Field memberValuesField = invocationHandler.getClass().getDeclaredField("memberValues");
                        memberValuesField.setAccessible(true);
                        Map valueMap = (Map) memberValuesField.get(invocationHandler);
                        Object message = valueMap.get("message");
                        if (Objects.nonNull(message)) {
                            return message.toString();
                        }
                    } catch (Exception e) {
                        log.error("解析字段提示语异常", e);
                    }
                    return null;
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());

        //去掉重复
        return Joiner.on("，").skipNulls().join(msgSet);
    }

    /**
     * <p>@Description : 解析表头，验证是否包含必须的表头 </p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2021/10/13 11:22 </p>
     * 注意：多行表头时，将每行表头名称组合形成：xx-xx-xx，因此验证必须表头时必须如此构造
     */
    @Override
    public void invokeHead(Map<Integer, CellData> headDataMap, AnalysisContext context) {
        ReadWorkbookHolder workbookHolder = context.readWorkbookHolder();
        Integer headRowNumber = workbookHolder.getHeadRowNumber();

        ReadRowHolder rowHolder = context.readRowHolder();
        if (rowHolder.getRowIndex() + 1 < headRowNumber) {
            return;
        }

        ExcelReadHeadProperty headProperty = workbookHolder.excelReadHeadProperty();
        Collection<Head> heads = headProperty.getHeadMap().values();
        headDataMap.forEach((idx, cell) -> {
            //获取最大列索引
            maxColumnIdx = idx > maxColumnIdx ? idx : maxColumnIdx;
            heads.forEach(head -> {
                if (head.getHeadNameList().contains(cell.getStringValue())) {
                    head.setColumnIndex(idx); //重置列索引，原来的是获取属性上@ExcelProperty的index值，重置为按表头定义索引，否则会出现定位错误
                    return;
                }
            });
        });

        //解析表头，形成头名称-列索引的Map对象
        Map<String, Head> headNameMap = Maps.newHashMap();
        //解析表头，形成字段名-列索引的Map对象
        Map<String, Head> headPropMap = Maps.newHashMap();
        heads.forEach(head -> {
            headNameMap.put(Joiner.on("-").skipNulls().join(head.getHeadNameList()), head);
            headPropMap.put(head.getFieldName(), head);
        });
        paramContext.setHeadNameMap(headNameMap);
        paramContext.setHeadPropMap(headPropMap);

        //判断是否包含必须的表头
        boolean isHeadFull = headNameMap.keySet().containsAll(this.getRequiredHead());
        if (!isHeadFull) {
            throw new ExcelCommonException("导入失败，表头错误，需包含必须列");
        }
    }

    /**
     * <p>@Description : 生成单元格样式 </p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2022/1/19 15:53 </p>
     */
    private CellStyle getResultStyle(Sheet sheet, IndexedColors colors) {
        Font font = sheet.getWorkbook().createFont();
        CellStyle style = sheet.getWorkbook().createCellStyle();
        font.setColor(colors.getIndex());
        font.setBold(true);
        style.setFont(font);
        return style;
    }

    /**
     * <p>@Description : 生成导入结果表格 </p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2022/1/19 16:05 </p>
     */
    private String generateResultExcel(AnalysisContext analysisContext) throws IOException {
        MultipartFile file = paramContext.getFile();
        if (Objects.isNull(file)) {
            log.warn("没有获取原始表格数据，无法回写结果表格");
            return null;
        }

        //解析原始excel文件的后缀名
        String suffix = StringUtils.getFilenameExtension(file.getOriginalFilename());
        ExcelTypeEnum excelType = ExcelTypeEnum.XLSX.name().equalsIgnoreCase(suffix) ? ExcelTypeEnum.XLSX : ExcelTypeEnum.XLS;

        //加载原始excel文件作为回写模板
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        ExcelWriter excelWriter = EasyExcel.write(output).excelType(excelType)
                .withTemplate(paramContext.getFile().getInputStream()).build();
        WriteSheet writeSheet = EasyExcel.writerSheet(0).build(); //注意：只支持单个sheet读取，并默认是第0个
        WriteContext writeContext = excelWriter.writeContext();
        writeContext.currentSheet(writeSheet, WriteTypeEnum.FILL);
        WriteSheetHolder writeSheetHolder = writeContext.writeSheetHolder();
        Sheet sheet = writeSheetHolder.getCachedSheet();

        //导入失败的样式
        CellStyle failStyle = this.getResultStyle(sheet, IndexedColors.RED);
        //导入成功的样式
        CellStyle successStyle = this.getResultStyle(sheet, IndexedColors.GREEN);

        //设置头的备注列
        int headRowNumber = analysisContext.readSheetHolder().getHeadRowNumber();
        Row headRow = sheet.getRow(headRowNumber - 1);
        org.apache.poi.ss.usermodel.Cell headCell = headRow.createCell(maxColumnIdx + 1);
        headCell.setCellValue("导入结果说明");

        //循环写入错误信息及标红
        dataList.forEach(data -> {
            Row row = sheet.getRow(data.getRowIdx());
            if (Objects.isNull(row)) {
                return;
            }
            //设置错误列的颜色
            data.getColumnIdxes().forEach(columnIdx -> {
                if (Objects.nonNull(columnIdx)) {
                    org.apache.poi.ss.usermodel.Cell valCell = row.getCell(columnIdx);
                    if (!Objects.isNull(valCell)) {
                        valCell.setCellStyle(failStyle);
                    }
                }
            });

            //设置备注内容及标色
            org.apache.poi.ss.usermodel.Cell remCell = row.createCell(maxColumnIdx + 1);
            remCell.setCellValue(data.getMessage());
            remCell.setCellStyle(data.isSuccess() ? successStyle : failStyle);
        });

        //关闭Excel写入流
        excelWriter.finish();
        output.close();

        //验证文件格式
        String fileName = StringUtils.stripFilenameExtension(file.getOriginalFilename());
        fileName = fileName.concat("_导入结果_").concat(paramContext.getOperatorName()).concat(".").concat(suffix);
        //return FileService.uploadFile(OSSDirectory.IMPORT, fileName, output.toByteArray());
        FileService fileService = SpringContextUtils.getBean(FileService.class);
        return fileService.upload(fileName, output.toByteArray(), OSSDirectory.IMPORT, false);
    }

    /**
     * <p>@Description : 导入完成之后，将错误信息生成表格后上传，并插入导入结果数据 </p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2021/10/13 11:34 </p>
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {

        //分析完后回调处理
        this.doFinish(analysisContext);

        //生成Excel处理结果
        String resultPath = "";
        try {
            resultPath = this.generateResultExcel(analysisContext);
        } catch (Exception e) {
            log.error("生成导入结果文件异常", e);
        }

        //统计成功\失败条数
        Map<Boolean, Long> countMap = dataList.stream().collect(Collectors.groupingBy(SuperImportVO::isSuccess, Collectors.counting()));

        //回调处理结果信息
        this.handleResult(analysisContext, dataList.size(),
                countMap.getOrDefault(Boolean.TRUE, 0L).intValue(),
                countMap.getOrDefault(Boolean.FALSE, 0L).intValue(), resultPath);
    }

    @Override
    public void invoke(T data, AnalysisContext context) {
        ReadRowHolder readRowHolder = context.readRowHolder();
        data.setRowIdx(readRowHolder.getRowIndex());
        dataList.add(data);

        //完善数据（合并单元格）
        this.mergeData(data, context);

        //验证数据
        if (!data.valid(paramContext, repeatSet)) {
            log.warn("数据验证失败：{}", data.toString());
            return;
        }
        //回调
        this.handleData(data, context);
    }

    /**
     * <p>@Description : 表头必须包含的列，多表头时需要将头名称组合 </p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2021/10/13 11:31 </p>
     */
    protected abstract Set<String> getRequiredHead();

    /**
     * <p>@Description : 分析完成后回调此方法 </p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2021/10/13 11:36 </p>
     */
    protected abstract void doFinish(AnalysisContext analysisContext);

    /**
     * <p>@Description : 上传结果文件后回调此方法 </p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2021/10/13 12:01 </p>
     *
     * @param analysisContext
     * @param failNum         失败条数
     * @param resultFilePath  导入结果文件路径
     */
    protected abstract void handleResult(AnalysisContext analysisContext, Integer totalNum, Integer successNum, Integer failNum, String resultFilePath);

    /**
     * <p>@Description : 每行处理业务逻辑回调 </p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2021/10/13 13:37 </p>
     */
    protected abstract void handleData(T data, AnalysisContext context);

    /**
     * <p>@Description : 合并数据，或者在验证之前需要对数据做处理的操作 </p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2022/1/19 15:36 </p>
     */
    protected void mergeData(T data, AnalysisContext context) {

    }
}
